import argparse
import json
import logging
import pickle

# from redislite import Redis
from datetime import datetime, timezone
from typing import Optional, Awaitable

from redis import Redis
from tornado import websocket, web, ioloop
from wrenchbox.logging import setup_log

from ourblockchain.transaction import Transaction
from ourblockchain.blockchain import Ledger, Block

DEFAULTS = {'port': 9000}
clients = []
db = None


class IndexHandler(web.RequestHandler):
    def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
        pass

    def get(self):
        logging.info("get")


class SocketHandler(websocket.WebSocketHandler):
    def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
        pass

    def on_message(self, message):
        try:
            message = json.loads(message)
        except json.JSONDecodeError:
            logging.warning('Cannot parse request message: %s', message)
            self.write_message(json.dumps({
                'status': 500,
                'error': 'Cannot parse request message.',
                'response': None
            }))
        else:
            if message is not None:
                if 'op' in message:
                    if message['op'] == 'register':
                        if 'args' in message and 'addr' in message['args']:
                            peers = pickle.loads(db.get('peers'))
                            if not isinstance(message['args']['addr'], list):
                                message['args']['addr'] = [str(message['args']['addr'])]
                            for addr in message['args']['addr']:
                                if addr.startswith('ws://') or addr.startswith('wss://'):
                                    peers.add(addr)
                            db.set('peers', pickle.dumps(peers))
                            self.write_message(json.dumps({
                                'status': 202,
                                'success': 'Accepted'
                            }))
                        else:
                            self.write_message(json.dumps({
                                'status': 500,
                                'error': 'Operation "register" requires the following "args": "addr"',
                                'response': None
                            }))
                    elif message['op'] == 'peers':
                        self.write_message(json.dumps({
                            'status': 200,
                            'success': 'Ok',
                            'response': {'peers': list(pickle.loads(db.get('peers')))}
                        }))
                    elif message['op'] == 'time':
                        self.write_message(json.dumps({
                            'status': 200,
                            'success': 'Ok',
                            'response': {
                                'time': datetime.now().timestamp(),
                                'time_zone': datetime.now(timezone.utc).astimezone().tzname()
                            }
                        }))
                    elif message['op'] == 'pool':
                        self.write_message(json.dumps({
                            'status': 200,
                            'success': 'OK',
                            'response': {
                                'pool': [json.loads(str(i)) for i in list(pickle.loads(db.get('pool')))]
                            }
                        }))
                    elif message['op'] == 'merge':
                        if 'args' in message and 'pool' in message['args']:
                            pool = pickle.loads(db.get('pool'))
                            ledger = pickle.loads(db.get('ledger'))
                            for i in message['args']['pool']:
                                # 若正在同步的item是setPrice类型，那么需要账本和pool中不存在该图片
                                if i['content']['purchase_type'] == 'setPrice':
                                    firstItem = ledger.getUrlFirstItem(i['content']['url'])
                                    if firstItem:  # 存在，则跳过
                                        continue
                                    jump_flag = False  # 检查pool
                                    for j in pool.copy():
                                        if i['content']['url'] == j.content.url:
                                            jump_flag = True
                                            break
                                    if jump_flag:
                                        continue

                                # 若正在同步的item是own类型
                                elif i['content']['purchase_type'] == 'own':
                                    # pool中又已存在同样url的own类型交易，那么当前item跳过
                                    jump_flag = False
                                    for j in pool.copy():
                                        if i['content']['url'] == j.content.url \
                                                and j.content.purchase_type == 'own':
                                            jump_flag = True
                                            break
                                    if jump_flag:
                                        continue

                                pool.add(Transaction(
                                    transaction_id=i['id'],
                                    sender=i['sender'],
                                    receiver=i['receiver'],
                                    amount=i['amount'],
                                    # 添加新的transaction中包含content信息
                                    content=i['content']
                                ))

                            # 账本里已存在同样的item或者该item购买的url已经易主，就不需要这条transaction
                            for i in pool.copy():
                                if ledger.find(i) or ledger.ownerChanged(i):
                                    pool.remove(i)

                            db.set('pool', pickle.dumps(pool))
                            self.write_message(json.dumps({
                                'status': 202,
                                'success': 'New Transactions Accepted in pool'
                            }))
                        elif 'args' in message and 'blocks' in message['args']:
                            pool = pickle.loads(db.get('pool'))
                            ledger = pickle.loads(db.get('ledger'))
                            for i in message['args']['blocks']:
                                # 账本长度为0或者当前block是账本的下一个block
                                if not len(ledger.blocks) or i['prev_hash'] == ledger.blocks[-1].hash():
                                    block = Block(prev_hash=i['prev_hash'])
                                    for j in i['items']:
                                        transaction = Transaction(
                                            transaction_id=j['id'],
                                            sender=j['sender'],
                                            receiver=j['receiver'],
                                            amount=j['amount'],
                                            t=j['t'],
                                            content=j['content'],
                                            prev_hash=j['prev_hash']
                                        )
                                        # 账本里已经存在当前同步的block中的item，代表该block发生了错误
                                        if ledger.find(transaction):
                                            self.write_message(json.dumps({
                                                'status': 500,
                                                'error': 'Duplication transaction in blockchain: {}'.format(transaction)
                                            }))
                                            return
                                        # 别的节点已经形成block并且同步过来了，所以当前节点缓存池的就不要了
                                        if transaction in pool:
                                            pool.remove(transaction)
                                        block.add(transaction)
                                    ledger.add(block)

                            try:
                                ledger.validate()
                            except AssertionError:
                                self.write_message(json.dumps({
                                    'status': 500,
                                    'error': 'Malicious block is detected: {}'.format(ledger.blocks[-1])
                                }))
                            else:
                                db.set('pool', pickle.dumps(pool))
                                db.set('ledger', pickle.dumps(ledger))
                                self.write_message(json.dumps({
                                    'status': 202,
                                    'success': 'New Blocks Accepted in blockchain'
                                }))
                        else:
                            self.write_message(json.dumps({
                                'status': 500,
                                'error': 'Operation "merge" requires the "args": "pool"',
                                'response': None
                            }))
                    elif message['op'] == 'blocks':
                        blocks = json.loads(str(pickle.loads(db.get('ledger'))))['blocks']
                        if 'args' in message and 'start' in message['args']:
                            blocks = blocks[int(message['args']['start']):]
                        self.write_message(json.dumps({
                            'status': 200,
                            'success': 'OK',
                            'response': {
                                'blocks': blocks
                            }
                        }))
                    else:
                        self.write_message(json.dumps({
                            'status': 404,
                            'error': 'Operation "{}" is not supported'.format(message['op']),
                            'response': None
                        }))
                else:
                    logging.warning('Message body is not supported: %s', message)
                    self.write_message(json.dumps({
                        'status': 500,
                        'error': 'Message body is not supported.',
                        'response': None
                    }))

    def check_origin(self, origin: str):
        return True

    def open(self):
        logging.info('Client connected: %s', self.request.remote_ip)
        if self not in clients:
            clients.append(self)

    def on_close(self):
        if self in clients:
            clients.remove(self)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument(
        '--debug',
        action='store_true',
        default=False,
        help='show debug information'
    )
    parser.add_argument(
        '-p', '--port',
        type=int,
        default=DEFAULTS['port'],
        help='listening port, default:{}'.format(DEFAULTS['port'])
    )
    parser.add_argument(
        '-r', '--redis',
        type=str,
        default='localhost',
        help='redis database file, default: redis.db'
    )
    parser.add_argument(
        '-rp', '--redis_port',
        type=int,
        default=6379,
        help="redis database's port, default:6379"
    )
    parser.add_argument(
        '--dbNum',
        type=int,
        default=0,
        help="num of redis database, default:0"
    )
    args, _ = parser.parse_known_args()
    print(args)

    setup_log(level=logging.DEBUG if args.debug else logging.INFO)
    db = Redis(host=args.redis, port=args.redis_port, db=args.dbNum)
    if b'peers' not in db.keys():
        db.set('peers', pickle.dumps(set([])))

    if b'pool' not in db.keys():
        db.set('pool', pickle.dumps(set([])))
    if b'ledger' not in db.keys():
        db.set('ledger', pickle.dumps(Ledger()))

    web.Application([
        (r'/web/(\w+)', IndexHandler),
        (r'/ws', SocketHandler)
    ]).listen(args.port)
    logging.info("Tornado is listening on port: %d", args.port)
    ioloop.IOLoop.instance().start()
